package com.dmai.capacity.platform.gateway.filter;


import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONObject;
import com.dmai.capacity.platform.common.constant.CommonConstant;
import com.dmai.capacity.platform.common.constant.SecurityConstants;
import com.dmai.capacity.platform.common.constant.UaaConstant;
import com.dmai.capacity.platform.common.dto.ResponseEntity;
import com.dmai.capacity.platform.common.dto.ResponseSimpleEntity;
import com.dmai.capacity.platform.common.model.GatewayLog;
import com.dmai.capacity.platform.common.model.OauthClient;
import com.dmai.capacity.platform.common.properties.SecurityProperties;
import com.dmai.capacity.platform.common.utils.DateUtils;
import com.dmai.capacity.platform.common.utils.WebfluxResponseUtil;
import com.dmai.capacity.platform.gateway.chain.SecurityFilterChain;
import com.dmai.capacity.platform.gateway.constants.ErrorCodeConstant;
import com.dmai.capacity.platform.gateway.constants.HandleResultEnum;
import com.dmai.capacity.platform.gateway.constants.RedisKeyConstant;
import com.dmai.capacity.platform.gateway.context.SecurityContext;
import com.dmai.capacity.platform.gateway.service.IPermissionService;
import com.dmai.capacity.platform.gateway.utils.ReactiveAddrUtil;
import com.dmai.capacity.platform.gateway.utils.ServerWebExchangeUtil;
import com.dmai.capacity.platform.log.enums.LogEnums;
import com.dmai.capacity.platform.log.model.Log;
import com.dmai.capacity.platform.log.trace.MDCTraceUtils;
import com.dmai.capacity.platform.log.util.BizLogUtil;
import com.dmai.capacity.platform.oauth.common.service.IOauthClientService;
import com.google.common.collect.Maps;
import eu.bitwalker.useragentutils.UserAgent;
import io.vavr.control.Try;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.reactivestreams.Publisher;
import org.redisson.Redisson;
import org.redisson.api.RAtomicLong;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.*;

import static com.dmai.capacity.platform.common.constant.CommonConstant.BEARER_TYPE;


@Slf4j
@Component
public class GatewayLogFilter implements GlobalFilter, Ordered {

    @Resource
    private SecurityFilterChain securityFilterChain;

    @Autowired
    private IOauthClientService clientService;

    @Autowired
    private IPermissionService permissionService;

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private Redisson redisson;


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();

        String requestPath = request.getPath().pathWithinApplication().value();
        String clientId = null;
        // 处理json太大直接通过oauth2鉴权时，https 出现connection reset by pear 异常问题
        if (securityProperties.getIgnore().getReAuthenticateUrls().contains(requestPath)) {
            clientId = verifyAuthenticate(request);
            if (StringUtils.isEmpty(clientId)) {
                return WebfluxResponseUtil.responseWrite(exchange, cn.hutool.http.HttpStatus.HTTP_UNAUTHORIZED, ResponseEntity.of(UaaConstant.AUTH_CODE_INVALID_TOKEN, "无效的Access_token", null));
            }
        }

        // 限流
        SecurityContext securityContext = new SecurityContext();
        securityContext.setExchange(exchange);
        try {
            securityFilterChain.execute(securityContext);
        } catch (Exception e) {
            return WebfluxResponseUtil.responseWrite(exchange, cn.hutool.http.HttpStatus.HTTP_BAD_REQUEST, ResponseEntity.of(ErrorCodeConstant.UNKNOWN_ERROR, "系统异常:{}", e.getMessage()));
        }
        if (securityContext.isResult()) {
            return WebfluxResponseUtil.responseWrite(exchange, securityContext.getCode(), securityContext.getEntity());
        }

        if (StringUtils.isEmpty(clientId)) {
            // 未授权，直接放过不记录数据库日志
            Map requestTrace = Try.of(() -> getRequestTrace(request))
                    .onFailure(ex -> log.error("requesttrace error", ex)).getOrElse(Maps.newHashMap());
            Long startTime = System.currentTimeMillis();
            return chain.filter(exchange.mutate().request(request.mutate().build()).build())
                    .then(Mono.fromRunnable(() -> {
                        Long endTime = System.currentTimeMillis();
                        boolean statusFlag = exchange.getResponse().getStatusCode().equals(HttpStatus.OK);
                        requestTrace.put("TimeElapsed", endTime - startTime);
                        requestTrace.put(CommonConstant.STATUS,
                                statusFlag ? CommonConstant.SUCCESS : CommonConstant.FAIL);
                        BizLogUtil.info(LogEnums.REQUEST_LOG.getTag(), LogEnums.REQUEST_LOG.getName(),
                                Log.builder().clientIp(ReactiveAddrUtil.getRemoteAddr(request))
                                        .requestParams(getParams(request.getQueryParams().toSingleValueMap()))
                                        .objectId(LogEnums.REQUEST_LOG.getId())
                                        .traceId(request.getHeaders().getFirst(MDCTraceUtils.TRACE_ID_HEADER))
                                        .spanId(request.getHeaders().getFirst(MDCTraceUtils.SPAN_ID_HEADER))
                                        .objectParam(requestTrace).build());
                    }));

        }


        // 接口权限判断
        OauthClient oauthClient = clientService.loadClientByClientId(clientId, requestPath);

        if (oauthClient == null || CollectionUtils.isEmpty(oauthClient.getCapabilitys())) {
            return WebfluxResponseUtil.responseWrite(exchange, cn.hutool.http.HttpStatus.HTTP_FORBIDDEN, ResponseEntity.of(ErrorCodeConstant.NOT_OPEN_CAPABILITY_PERMISSION, "未开通接口调用能力，请联系管理员", null));
        }
        if (oauthClient.getCapabilitys().get(0).getExpiredTime() != null && DateUtils.getNowDate().compareTo(oauthClient.getCapabilitys().get(0).getExpiredTime()) >= 0) {
            return WebfluxResponseUtil.responseWrite(exchange, cn.hutool.http.HttpStatus.HTTP_FORBIDDEN, ResponseEntity.of(ErrorCodeConstant.PERMISSION_EXPIRED, "接口调用能力已过期，请联系管理员", null));
        }
        // 检查当前请求用户是否可通资源包或者按量后付费
        Integer packageId = permissionService.checkPermission(oauthClient.getUserid(), oauthClient == null ? 0 : oauthClient.getCapabilitys().get(0).getCapabilityId());
        if (packageId == null) {
            RAtomicLong atomicLong = redisson.getAtomicLong(RedisKeyConstant.CACHE_USER_REQUEST_COUNT_PREFIX + ":" + oauthClient.getUserid() + ":" + oauthClient.getCapabilitys().get(0).getCapabilityId());
            if (atomicLong.isExists() && atomicLong.get() > 0) {
                return WebfluxResponseUtil.responseWrite(exchange, cn.hutool.http.HttpStatus.HTTP_FORBIDDEN, ResponseEntity.of(ErrorCodeConstant.NO_PERMISSION_AVAILABLE, "限制接口访问，您有" + atomicLong.get() + "个任务在请求中，请稍后重试", null));
            } else {
                return WebfluxResponseUtil.responseWrite(exchange, cn.hutool.http.HttpStatus.HTTP_FORBIDDEN, ResponseEntity.of(ErrorCodeConstant.NO_PERMISSION_AVAILABLE, "限制接口访问，请联系管理员或稍后再试", null));
            }
        }

        String traceId = request.getHeaders().get(CommonConstant.TRACE_ID_HEADER).get(0);
        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        GatewayLog gatewayLog = new GatewayLog();
        gatewayLog.setUserid(oauthClient.getUserid());
        gatewayLog.setClientId(clientId);
        gatewayLog.setCapabilityId(oauthClient.getCapabilitys().get(0).getCapabilityId());
        gatewayLog.setPackageId(packageId);
        gatewayLog.setTraceId(traceId);
        gatewayLog.setRequestUrl(requestPath);
        gatewayLog.setTargetServer(route.getUri().toString());
        gatewayLog.setRequestTime(new Date());
        gatewayLog.setRequestParams(getParams(request.getQueryParams().toSingleValueMap()));
        gatewayLog.setClientIp(request.getRemoteAddress().getHostString());
        gatewayLog.setAsync(false);//默认非异步请求

        String requestBody = ServerWebExchangeUtil.getBodyContent(exchange);
//        gatewayLog.setRequestParams(requestBody); //图片base64太大了，不打印

        //通过请求参数判断是否为异步调用
        boolean isAsync = true;
        try {
            //todo 待优化，body太大转对象很慢
            JSONObject jsonObject = JSONObject.parseObject(requestBody);
            if (jsonObject.containsKey("async")) {
                isAsync = jsonObject.getBoolean("async");
            }
        } catch (Exception e) {
        }
//                    gatewayLog.setRequestBody(requestBody); // 太大了，不能写日志
        gatewayLog.setAsync(isAsync);

        ServerWebExchange build = exchange.mutate().build();
        if (CollectionUtils.isEmpty(request.getHeaders().get(SecurityConstants.X_CLIENT_HEADER))) {
            String finalClientId = clientId;
            ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate()
                    .headers(h -> h.add(SecurityConstants.X_CLIENT_HEADER, finalClientId))
                    .build();

            build = exchange.mutate().request(serverHttpRequest).build();
        }

        //获取响应体
        ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);

        return chain.filter(build.mutate().response(decoratedResponse).build())
                .then(Mono.fromRunnable(() -> {
                    triggerRequestEvent(gatewayLog, exchange);
                }));
    }

    @Override
    public int getOrder() {
        return -1100;
    }


    /**
     * 保存请求记录
     */
    private void triggerRequestEvent(GatewayLog gatewayLog, ServerWebExchange exchange) {
        Map requestTrace = Try.of(() -> getRequestTrace(exchange.getRequest()))
                .onFailure(ex -> log.error("requesttrace error", ex)).getOrElse(Maps.newHashMap());
        BizLogUtil.info(LogEnums.REQUEST_LOG.getTag(), LogEnums.REQUEST_LOG.getName(),
                Log.builder().traceId(gatewayLog.getTraceId())
                        .clientIp(gatewayLog.getClientIp())
                        .requestTime(gatewayLog.getRequestTime())
                        .requestParams(gatewayLog.getRequestParams())
                        .objectId(LogEnums.REQUEST_LOG.getId())
                        .responseCode(gatewayLog.getResponseCode())
                        .responseData(gatewayLog.getResponseData())
                        .objectParam(requestTrace)
                        .build());

        applicationEventPublisher.publishEvent(gatewayLog);
    }


    /**
     * 记录响应日志
     * 通过 DataBufferFactory 解决响应体分段传输问题。
     */
    private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLog gatewayLog) {
        ServerHttpResponse response = exchange.getResponse();
        DataBufferFactory bufferFactory = response.bufferFactory();

        return new ServerHttpResponseDecorator(response) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    gatewayLog.setResponseTime(new Date());

                    gatewayLog.setResponseCode(String.valueOf(response.getStatusCode().value()));

                    // 获取响应类型
                    String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);


                    if (Objects.equals(this.getStatusCode(), HttpStatus.OK) && StringUtils.isNotBlank(originalResponseContentType) && originalResponseContentType.contains(MediaType.APPLICATION_JSON_VALUE)) {

                        Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                        return super.writeWith(fluxBody.buffer().map(dataBuffers -> {

                            // 合并多个流集合，解决返回体分段传输
                            DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                            DataBuffer join = dataBufferFactory.join(dataBuffers);
                            byte[] content = new byte[join.readableByteCount()];
                            join.read(content);

                            // 释放掉内存
                            DataBufferUtils.release(join);
                            String responseResult = new String(content, StandardCharsets.UTF_8);
                            ResponseSimpleEntity responseSimple = JSONObject.parseObject(responseResult, ResponseSimpleEntity.class);
                            if (gatewayLog.getAsync() == null || !gatewayLog.getAsync()) {
                                gatewayLog.setHandleResult(0 == responseSimple.getCode() ? HandleResultEnum.SUCCESS.getCode() : HandleResultEnum.FAIL.getCode());
                            }
                            gatewayLog.setResponseData(JSONObject.toJSONString(responseSimple));

                            return bufferFactory.wrap(content);
                        }));
                    }
                }
                // if body is not a flux. never got there.
                return super.writeWith(body);
            }
        };
    }


    /**
     * 获取请求日志
     *
     * @param request 请求
     * @return requestLog
     */
    private Map getRequestTrace(ServerHttpRequest request) {
        // 埋点
        Map requestTrace = Maps.newHashMap();
        requestTrace.put("RemoteAddr", ReactiveAddrUtil.getRemoteAddr(request));
        requestTrace.put("Uri", request.getURI().getRawPath());
        requestTrace.put("Method", request.getMethodValue());
        requestTrace.put("User-Agent", request.getHeaders().getFirst(HttpHeaders.USER_AGENT));
        UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeaders().getFirst(HttpHeaders.USER_AGENT));
        requestTrace.put("Browser", ReactiveAddrUtil.getBrowser(userAgent.getBrowser().name()));
//        requestTrace.put("OperatingSystem", ReactiveAddrUtil.getOperatingSystem(userAgent.getOperatingSystem().name()));
//        requestTrace.put("CreatTime", LocalDateTime.now());
        return requestTrace;
    }

    /**
     * 获取请求参数
     *
     * @param parameterMap 请求参数
     * @return params
     */
    private String getParams(Map<String, String> parameterMap) {
        return HttpUtil.toParams(parameterMap);
    }

    /**
     * 判断是否鉴权
     *
     * @return
     */
    private String verifyAuthenticate(@NotNull ServerHttpRequest request) {
        boolean hasAuthorization = request.getHeaders().containsKey(CommonConstant.TOKEN_HEADER);
        if (!hasAuthorization) {
            return null;
        }
        String accessTokenValue = request.getHeaders().get(CommonConstant.TOKEN_HEADER).get(0);

        OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue.replaceFirst(BEARER_TYPE, "").trim());
        if (accessToken == null) {
            return null;
        } else if (accessToken.isExpired()) {
            tokenStore.removeAccessToken(accessToken);
            return null;
        }
        OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
        if (result == null) {
            return null;
        }
        return result.getOAuth2Request().getClientId();
    }
}